# -*- mode: python; python-indent-offset: 4; indent-tabs-mode: nil -*-
##
# This script includes generic build options:
#    release/debug, target os, target arch, cross toolchain, build environment etc
##
import os
import sys
import platform
import re

project_version = '2.0.1'

# Map of build host to possible target os
host_target_map = {
    'linux': ['linux', 'android', 'yocto', 'tizen', 'webos'],
    'windows': ['windows', 'android'],
    'darwin': ['darwin', 'ios', 'android'],
    'msys_nt': ['msys_nt'],
}

# Map of target os to possible target architecture
os_arch_map = {
    'linux': [
        'x86', 'x86_64', 'arm', 'armv7l', 'arm-v7a', 'armeabi-v7a', 'arm64', 'mips',
        'mipsel', 'mips64', 'mips64el', 'i386', 'powerpc', 'sparc', 'aarch64',
        'armv6l', 'armv7l'
    ],
    'tizen': ['x86', 'x86_64', 'arm', 'arm-v7a', 'armeabi-v7a', 'arm64', 'armv7l'],
    'webos': ['arm'],
    'android': [
        'x86', 'x86_64', 'armeabi', 'armeabi-v7a', 'armeabi-v7a-hard',
        'arm64-v8a'
    ],
    'windows': ['x86', 'amd64', 'arm'],
    'msys_nt': ['x86', 'x86_64'],
    'darwin': ['i386', 'x86_64'],
    'ios': ['i386', 'x86_64', 'armv7', 'armv7s', 'arm64'],
    'yocto': [
        'i586', 'i686', 'x86_64', 'arm', 'aarch64', 'powerpc', 'powerpc64',
        'mips', 'mipsel'
    ],
}

# Fetch host from Python platform information and smash case
host = platform.system().lower()

# In case of msys_nt, the host string contains an internal Windows version
# which is not interesting for lookups in the maps.
# Canonicalize the msys_nt-XX.X system name to msys-nt
if 'msys_nt' in host:
    host = 'msys_nt'

if host not in host_target_map:
    msg = "\nError: building on host os '%s' is not currently supported.\n" % host
    Exit(msg)

######################################################################
# Get build options from command line
######################################################################
target_os = ARGUMENTS.get('TARGET_OS', host).lower()

if target_os not in host_target_map[host]:
    msg = "\nError: host '%s' cannot currently build target '%s'" % (host, target_os)
    msg += "\n\tchoices: %s\n" % host_target_map[host]
    Exit(msg)

# work out a reasonable default for target_arch if not specified by user
if target_os == 'android':
    default_arch = 'x86'
elif target_os == 'webos':
    default_arch = 'arm'
else:
    default_arch = platform.machine()
    if target_os == 'windows':
        default_arch = default_arch.lower()
    if target_os == 'linux' and default_arch in ('i586', 'i686'):
        default_arch = 'x86'
    if target_os == 'tizen' and default_arch in ('i586', 'i686'):
        default_arch = 'x86'

target_arch = ARGUMENTS.get('TARGET_ARCH', default_arch)  # target arch

default_with_upstream_libcoap = 0

if ARGUMENTS.get('TEST'):
    logging_default = False
else:
    release_mode = False
    if ARGUMENTS.get('RELEASE', True) in [
            'y', 'yes', 'true', 't', '1', 'on', 'all', True
    ]:
        release_mode = True
    logging_default = (release_mode is False)

# generate a list of unique targets: convert to set() for uniqueness,
# then convert back to a list
targetlist = list(set(x for l in list(host_target_map.values()) for x in l))

######################################################################
# Common build options
######################################################################

help_vars = Variables()
help_vars.AddVariables(
    ('PROJECT_VERSION',
                 'The version of IoTivity',
                 project_version),
    BoolVariable('VERBOSE',
                 'Show compilation',
                 default=False),
    BoolVariable('RELEASE',
                 'Build for release?',
                 default=True),
    EnumVariable('TARGET_OS',
                 'Target platform',
                 default=host,
                 allowed_values=targetlist),
    EnumVariable('SECURED',
                 'Build with DTLS',
                 default='1',
                 allowed_values=('0', '1')),
    ListVariable('TARGET_TRANSPORT',
                 'Target transport',
                 default='ALL',
                 names=('ALL', 'BT', 'BLE', 'IP', 'NFC')),
    BoolVariable('WITH_TCP',
                 'Build with TCP adapter',
                 default=False),
    BoolVariable('WITH_PROXY',
                 'Build with CoAP-HTTP Proxy',
                 default=True),
    ListVariable('WITH_MQ',
                 'Build with MQ publisher/broker',
                 default='OFF',
                 names=('OFF', 'SUB', 'PUB', 'BROKER')),
    BoolVariable('WITH_CLOUD',
                 'Build including AccountManager class and Cloud Client sample',
                 default=False),
    BoolVariable('WITH_TEST',
                 'Build unit tests',
                 default=True),
    ListVariable('RD_MODE',
                 'Resource Directory build mode',
                 default='CLIENT',
                 names=('CLIENT', 'SERVER')),
    BoolVariable('SIMULATOR',
                 'Build with simulator module',
                 default=False),
    EnumVariable('TARGET_ARCH',
                 'Target architecture',
                 default=default_arch,
                 allowed_values=os_arch_map[target_os]),
    EnumVariable('MULTIPLE_OWNER',
                 'Enable multiple owner',
                 default='0',
                 allowed_values=('0', '1')),
    EnumVariable('EXC_PROV_SUPPORT',
                 'Except OCPMAPI library(libocpmapi.so)',
                 default='0',
                 allowed_values=('0', '1')),
    EnumVariable('TEST',
                 'Run unit tests',
                 default='0',
                 allowed_values=('0', '1')),
    BoolVariable('LOGGING',
                 'Enable stack logging',
                 default=logging_default),
    EnumVariable('LOG_LEVEL',
                 'Enable stack logging level',
                 default='DEBUG',
                 allowed_values=('DEBUG', 'INFO', 'ERROR', 'WARNING', 'FATAL')),
    EnumVariable('ROUTING',
                 'Enable routing',
                 default='EP',
                 allowed_values=('GW', 'EP')),
    EnumVariable('WITH_UPSTREAM_LIBCOAP',
                 'Use latest stable version of LibCoAP downloaded from github',
                 default=default_with_upstream_libcoap,
                 allowed_values=('0', '1')),
    EnumVariable('BUILD_SAMPLE',
                 'Build with sample',
                 default='ON',
                 allowed_values=('ON', 'OFF')),
    # ANDROID_NDK set at end after default computed
    # ANDROID_HOME set at end after default computed
    # ANDROID_GRADLE set at end after default computed
    BoolVariable('WITH_ENV',
                 'Use compiler options from environment',
                 default=False),
    BoolVariable('AUTOMATIC_UPDATE',
                 'Makes libcoap update automatically to the required versions if needed.',
                 default=False),
    BoolVariable('BUILD_JAVA',
                 'Build Java bindings',
                 default=False),
    PathVariable('JAVA_HOME',
                 'JDK directory',
                 default=os.environ.get('JAVA_HOME'),
                 validator=PathVariable.PathAccept),
    EnumVariable('OIC_SUPPORT_TIZEN_TRACE',
                 'Tizen Trace(T-trace) api availability',
                 default=False,
                 allowed_values=('True', 'False')),
    BoolVariable('MANDATORY',
                 'Enable/disable(default) mandatory',
                 default=False)
)

######################################################################
# Platform (build target) specific options
######################################################################
if target_os in ['linux']:
    # Build option to enable failing build if warnings encountered.
    # May need to be off for developing with newer compilers which
    # are stricter about emitting warnings. Defaults to true so
    # developer builds and gerrit builds have all warnings examined.
    help_vars.Add(
        BoolVariable('ERROR_ON_WARN',
                     'Make all compiler warnings into errors.',
                      default=False))

targets_support_valgrind = ['linux', 'darwin']
if target_os in targets_support_valgrind:
    # Build option to enable unit tests to be run under valgrind.
    # May need to disable on small-memory platforms (e.g. Raspberry Pi)
    # Checking for memory leak and usage errors is part of the
    # acceptance criteria for project code, so this should default to on.
    help_vars.Add(
        BoolVariable('VALGRIND_CHECKS',
                     'Build support for running code coverage checks',
                      default=False))

targets_support_gcov = ['linux', 'darwin']
if target_os in targets_support_gcov:
    # Build option to enable coverage checking using gcov.
    # Requires gcc or clang family compilers.
    # Actual compiler flags need to be set in target-specific script.
    help_vars.Add(
        BoolVariable('COVERAGE_CHECKS',
                     'Build support for running code coverage checks',
                      default=False))

if target_os == 'windows':
    # Builds differ based on Visual Studio version
    #   For VS2013, MSVC_VERSION is '12.0'.
    #   For VS2015, MSVC_VERSION is '14.0'.
    #   For VS2017, MSVC_VERSION is '14.1'.
    # Default value is None, which means SCons will pick
    help_vars.Add(
        EnumVariable('MSVC_VERSION',
                     'MSVC compiler version - Windows',
                     default=None,
                     allowed_values=('12.0', '14.0', '14.1')))
    help_vars.Add(
        EnumVariable('MSVC_UWP_APP',
                     'Build a Universal Windows Platform (UWP) Application',
                     default='0',
                     allowed_values=('0', '1')))

AddOption(
    '--prefix',
    dest='prefix',
    type='string',
    nargs=1,
    action='store',
    metavar='DIR',
    help='installation prefix')

######################################################################
# Platform (build target) specific options: SDK/NDK & toolchain
######################################################################
targets_support_cc = ['linux', 'tizen']

if target_os in targets_support_cc:
    # Set cross compile toolchain
    help_vars.Add('TC_PREFIX',
                  "Toolchain prefix (Generally only required for cross-compiling)",
                  default=None)
    help_vars.Add(
        PathVariable('TC_PATH',
                     'Toolchain path (Generally only required for cross-compiling)',
                     default=None,
                     validator=PathVariable.PathAccept))

######################################################################
# this is where the setup of the construction envionment begins
######################################################################
if target_os in ['android']:
    # Android always uses GNU compiler regardless of the host
    env = Environment(
        variables=help_vars,
        tools=['gnulink', 'gcc', 'g++', 'ar', 'as', 'textfile'])
else:
    env = Environment(
        variables=help_vars,
        tools=['default', 'textfile'],
        TARGET_ARCH=target_arch,
        TARGET_OS=target_os,
        PREFIX=GetOption('prefix'),
        LIB_INSTALL_DIR=ARGUMENTS.get('LIB_INSTALL_DIR')  #for 64bit build
    )

if env.get('WITH_ENV'):
    env['ENV'] = os.environ
    if 'CC' in os.environ:
        env['CC'] = Split(os.environ['CC'])
        print("using CC from environment: %s" % env['CC'])
    if 'CXX' in os.environ:
        env['CXX'] = Split(os.environ['CXX'])
        print("using CXX from environment: %s" % env['CXX'])
    if 'CFLAGS' in os.environ:
        env['CFLAGS'] = Split(os.environ['CFLAGS'])
        print("using CFLAGS from environment: %s" % env['CFLAGS'])
    if 'CXXFLAGS' in os.environ:
        env['CXXFLAGS'] = Split(os.environ['CXXFLAGS'])
        print("using CXXFLAGS from environment: %s" % env['CXXFLAGS'])
    if 'CCFLAGS' in os.environ:
        env['CCFLAGS'] = Split(os.environ['CCFLAGS'])
        print("using CCFLAGS from environment: %s" % env['CCFLAGS'])
    if 'CPPFLAGS' in os.environ:
        env['CPPFLAGS'] = Split(os.environ['CPPFLAGS'])
        print("using CPPFLAGS from environment: %s" % env['CPPFLAGS'])
    if 'LDFLAGS' in os.environ:
        env['LINKFLAGS'] = Split(os.environ['LDFLAGS'])
        print("using LDFLAGS/LINKFLAGS from environment: %s" % env['LINKFLAGS'])

# set quieter build messages unless verbose mode was requested
if not env.get('VERBOSE'):
    env['CCCOMSTR'] = "Compiling $TARGET"
    env['SHCCCOMSTR'] = "Compiling $TARGET"
    env['CXXCOMSTR'] = "Compiling $TARGET"
    env['SHCXXCOMSTR'] = "Compiling $TARGET"
    env['LINKCOMSTR'] = "Linking $TARGET"
    env['SHLINKCOMSTR'] = "Linking shared object $TARGET"
    env['ARCOMSTR'] = "Archiving $TARGET"
    env['RANLIBCOMSTR'] = "Indexing Archive $TARGET"

if env.get('MANDATORY'):
    env.AppendUnique(CPPDEFINES=['__MANDATORY__'])

tc_set_msg = '''
************************************ Warning **********************************
* Warning: TC_PREFIX and/or TC_PATH is set in the environment.
* This means a non-default compilation toolchain will be used.
* If this is not what you expected you should unset, or it
* may lead to unexpected results.
*******************************************************************************
'''
if target_os in targets_support_cc:
    prefix = env.get('TC_PREFIX')
    tc_path = env.get('TC_PATH')
    if prefix:
        env.Replace(CC=prefix + env.get('CC', 'gcc'))
        env.Replace(CXX=prefix + env.get('CXX', 'g++'))
        env.Replace(AR=prefix + env.get('AR', 'ar'))
        env.Replace(AS=prefix + env.get('AS', 'as'))
        env.Replace(RANLIB=prefix + env.get('RANLIB', 'ranlib'))

    if tc_path:
        env.PrependENVPath('PATH', tc_path)
        sys_root = os.path.abspath(tc_path + '/../')
        env.AppendUnique(CCFLAGS=['--sysroot=' + sys_root])
        env.AppendUnique(LINKFLAGS=['--sysroot=' + sys_root])

    if prefix or tc_path:
        print(tc_set_msg)

# Import env variables only if reproductibility is ensured
if target_os in ['yocto', 'webos']:
    env['CONFIG_ENVIRONMENT_IMPORT'] = True
else:
    env['CONFIG_ENVIRONMENT_IMPORT'] = False

if env['CONFIG_ENVIRONMENT_IMPORT']:
    print("warning: importing some environment variables for OS: %s" % target_os)
    for ev in [
        'PATH',
        'PKG_CONFIG',
        'PKG_CONFIG_PATH',
        'PKG_CONFIG_SYSROOT_DIR'
    ]:
        if os.environ.get(ev) is not None:
            env['ENV'][ev] = os.environ.get(ev)
    if os.environ['LDFLAGS'] != None:
        env.AppendUnique(LINKFLAGS=Split(os.environ['LDFLAGS']))

if host in ['windows']:
    # UnpackAll.py needs access to system PATH components that SCons
    # does not include by default - e.g., the path to 7z.exe.
    env.AppendUnique(PATH=os.environ['PATH'])

# Ensure scons is able to change its working directory
env.SConscriptChdir(1)

# To cut down how much is built, enable the Default(DefTarg) line.
# Only those targets added to the alias "DefaultTargets" will
# then be built by default, else scons will by default build
# all targets it finds. "DefaultTargets" is added to by calls
# to InstallTarget and AppendTarget, unless those methods are
# passed the argument default=False
#
DefTarg = Alias("DefaultTargets")
# Default(DefTarg)

######################################################################
# Convenience functions to "extend" SCons
#
# Functions are added into scope of the rest of the build system
# by calling AddMethod
#
# These should probably move to site_init for better findability
######################################################################

def SetDir(env, dir):
    """
    Set up the target directory for building.

    Sets up the scons Variant directory, which is where objects
    built under scons control will go, rather than in the source tree.
    Also shortcuts some of the available scons mechansims by saving the
    paths in the construction environment. Since this sets the
    variant dir, this should be called only once, using the main
    main construction environment. These are retrieved by:
        env.get('SRC_DIR')
        env.get('BUILD_DIR')

    SRC_DIR will be the passed directory, while BUILD_DIR is
    constructed based on target system/arch and release mode.
    On Windows:
        'dir'/out/windows/<win32 or uwp>/<target_arch>/<release or debug>/

    On all others:
        'dir'/out/<target_os>/<target_arch>/<release or debug>/

    Args:
        env: construction environment
        dir: the source directory
    """

    if not os.path.exists(dir + '/SConstruct'):
        msg = '''
*************************************** Error *********************************
* The directory (%s) seems not to be a buildable directory,
* no SConstruct file found.
*******************************************************************************
''' % dir
        Exit(msg)

    build_dir = dir + '/out/' + target_os + '/'

    if target_os == 'windows':
        if env.get('MSVC_UWP_APP') == '1':
            build_dir = build_dir + 'uwp/'
        else:
            build_dir = build_dir + 'win32/'

    build_dir = build_dir + target_arch

    if env.get('RELEASE'):
        build_dir = build_dir + '/release/'
    else:
        build_dir = build_dir + '/debug/'

    env.VariantDir(build_dir, dir, duplicate=0)

    env.Replace(BUILD_DIR=build_dir)
    env.Replace(SRC_DIR=dir)

env.AddMethod(SetDir)


def SrcToObj(env, src, home=''):
    """
    Build an object in BUILD_DIR.

    Uses BUILD_DIR, SetDir should have been called previously.
    Note this is used only in the android compat script.

    Args:
        env: construction environment
        src: source directory
        home: the part of the path to chop off
    """
    obj = env.get('BUILD_DIR') + src.replace(home, '')
    if env.get('OBJSUFFIX'):
        obj += env.get('OBJSUFFIX')
    return env.Object(obj, src)

env.AddMethod(SrcToObj)


def InstallTarget(ienv, targets, name=None, default=True):
    """
    Copy a built target to the common location.

    The iotivity build puts the libraries and executables in a common
    location to make it easy to run the executables in place without
    worrying about the rpath (for systems that have it), or to account
    for no rpath option being available (Windows systems).  This not
    the same as "installing", see the UserInstall* methods for that.

    As side effects, the target is added to either the list of default
    targets or non-default targets, which are later handed to the Help
    function to help display options to the user.  If this is a default
    target, it is also added to the DefaultTargets alias.

    Args:
        ienv: the construction environment of the caller.
        targets: list of targets to copy.
        name: if set, the name to copy to; else use the target name.
        default: whether this is a default target or not.

    Notes:
        If used for copying a library, a race condition may occur on
        Windows systems if parallel building is enabled.  The separate
        operations from the SharedLibrary builder and the Command builder
        calling Copy may see Python release the Global Interpreter Lock
        in between steps, and another task may conclude the library
        is ready before the copy has taken place, and then report a
        missing dependency. A workaround is to generate the library
        directly into the common location so it does not need to be
        copied after building.

    See Also:
        AppendTarget
    """
    for filename in ienv.GetBuildPath(targets):
        basename = os.path.basename(filename)
        dst = env.get('BUILD_DIR') + basename
        i_n = Command(dst, filename, Copy("$TARGET", "$SOURCE"))
        if not name:
            name = basename
        ienv.Alias(name, i_n)

        # note the aliases are added to the target lists in the
        # main construction environment, not the one we're called from
        if default:
            env.AppendUnique(DefaultBuild=[name])
            Alias("DefaultTargets", name)
        else:
            env.AppendUnique(NoDefaultBuild=[name])

env.AddMethod(InstallTarget)


def AppendTarget(ienv, name, targets=None, default=True):
    """
    Add targets to target list.

    The target 'name' is added to either the list of default targets
    or non-default targets, which are later handed to the Help function
    to help display options to the user.  If this is a default target,
    it is also added to the DefaultTargets alias.

    Args:
        ienv: the construction environment of the caller.
        name: target name to add.
        targets: aliases the 'name' target will expand to.
        default: whether this is a default target or not.

    See Also:
        AppendTarget
    """
    if targets:
        env.Alias(name, targets)
    if default:
        env.AppendUnique(DefaultBuild=[name])
        Alias("DefaultTargets", name)
    else:
        env.AppendUnique(NoDefaultBuild=[name])

env.AddMethod(AppendTarget)


def UserInstallTargetLib(ienv, targets, name=None):
    """
    Install built libraries to a "system" location.

    Install files to system location, when running "scons install".
    If PREFIX is set it is used with a 'lib' suffix, unless
    LIB_INSTALL_DIR is also set in which case it is used unchanged.
    If neither, a 'deploy' subdirectory of the common build location
    is used. Unless deploy was used, the rpath is stripped.

    Args:
        ienv: construction environment of caller
        targets: libraries to install
        name: currently unused (future: installation name)
    """
    def __chrpath(target, source, env):
        """
        Remove RPATH if present.
        """
        if target_os in ['linux', 'tizen']:
            env.Command(None, target, 'chrpath -d $SOURCE')

    user_prefix = env.get('PREFIX')
    if user_prefix:
        user_lib = env.get('LIB_INSTALL_DIR')
        if user_lib:
            dst_dir  = user_lib
        else:
            dst_dir  = user_prefix + '/lib'
    else:
        dst_dir  = env.get('BUILD_DIR') + '/deploy'
    action = ienv.Install(dst_dir, targets)
    if not user_prefix and str(targets[0]).endswith(env['SHLIBSUFFIX']):
        ienv.AddPostAction(action, __chrpath)
    ienv.Alias("install", action)

env.AddMethod(UserInstallTargetLib)


def UserInstallTargetBin(ienv, targets, name=None):
    """
    Install built binaries to a "system" location.

    Install files to system location, when running "scons install".
    If PREFIX is set it is used with a 'bin' suffix, else a 'deploy'
    subdirectory of the common build location is used.

    Args:
        ienv: construction environment of caller
        targets: libraries to install
        name: currently unused (future: installation name)
    """
    user_prefix = env.get('PREFIX')
    if user_prefix:
        dst_dir  = user_prefix + '/bin'
    else:
        dst_dir  = env.get('BUILD_DIR') + '/deploy'
    ienv.Alias("install", ienv.Install(dst_dir , targets))

env.AddMethod(UserInstallTargetBin)


def UserInstallTargetHeader(ienv, targets, dir="", name=None):
    """
    Install header files to a "system" location.

    Install files to system location, when running "scons install".
    If PREFIX is set it is used with an 'include/iotivity' suffix, else a
    'deploy/include' subdirectory of the common build location is used.
    If dir is present, it as appended to the installation path.

    Args:
        ienv: construction environment of caller
        targets: headers to install
        dir: additional subdirectory to use in install location
        name: currently unused (future: installation name)
    """
    user_prefix = env.get('PREFIX')
    if user_prefix:
        i_n = ienv.Install(user_prefix + '/include/iotivity/' + dir, targets)
    else:
        i_n = ienv.Install(env.get('BUILD_DIR') + 'deploy/include/' + dir, targets)
    ienv.Alias("install", i_n)

env.AddMethod(UserInstallTargetHeader)


def UserInstallTargetPCFile(ienv, targets, name=None):
    """
    Install pkg-config files to a "system" location.

    Install files to system location, when running "scons install".
    If PREFIX is set it is used with a 'lib/pkgconfig' suffix, unless
    LIB_INSTALL_DIR is also set in which case it is used with a 'pkgconfig'
    suffix.  If neither, a 'deploy/pkgconfig' subdirectory of the common
    build location is used.

    Args:
        ienv: construction environment of caller
        targets: files to install
        name: currently unused (future: installation name)
    """
    user_prefix = env.get('PREFIX')
    if user_prefix:
        user_lib = env.get('LIB_INSTALL_DIR')
        if user_lib:
            i_n = ienv.Install(user_lib + '/pkgconfig', targets)
        else:
            i_n = ienv.Install(user_prefix + '/lib/pkgconfig', targets)
    else:
        i_n = ienv.Install(env.get('BUILD_DIR') + 'deploy/pkgconfig', targets)
    ienv.Alias("install", i_n)

env.AddMethod(UserInstallTargetPCFile)


def UserInstallTargetExtra(ienv, targets, subdir=None):
    """
    Install files to system location.

    Install "extra" files not covered by the other system-install methods,
    such as tests, xml files, security data files, etc., when doing
    "scons install". If PREFIX is set it is used with a 'lib/iotivity' suffix,
    unless LIB_INSTALL_DIR is also set in which case it is used with an
    'iotivity' suffix. If neither, a 'deploy/extra' subdirectory of the
    common build location is used.  If subdir is set, it is added to
    the end of whichever install path is selected. If not set, the
    tail of the path is computed.

    Args:
        ienv: construction environment of caller
        targets: files to install
        subdir: if set, use as the tail of the installation path
    """
    user_lib = env.get('LIB_INSTALL_DIR')
    user_prefix = env.get('PREFIX')
    if subdir is None:
        subdir = Dir('.').srcnode().path
    if user_lib:
        dst = user_lib + '/iotivity/' + subdir
    elif user_prefix:
        dst = user_prefix + '/lib/iotivity/' + subdir
    else:
        dst = env.get('BUILD_DIR') + '/deploy/extra/' + subdir
    for target in targets:
        i_n = ienv.Install(dst, target)
        ienv.Alias('install', i_n)

env.AddMethod(UserInstallTargetExtra)


def AddPthreadIfNeeded(ienv):
    """
    Set a linker flag to enable POSIX threads, if needed.

    Args:
        ienv: construction environment of caller
    """
    if 'gcc' == ienv.get('CC') and target_os not in ['android']:
        ienv.AppendUnique(LINKFLAGS="-pthread")

env.AddMethod(AddPthreadIfNeeded)


def PrintTargets(env):
    """
    Add message about IoTivity build targets to help message.

    Args:
        env: construction envrionment
    """
    Help("""
===============================================================================
Default Targets:\n    """)
    for t in env.get("DefaultBuild"):
        Help(t + ' ')

    if env.get("NoDefaultBuild"):
        Help("\n\nNon-default Targets:\n    ")
        for t in env.get('NoDefaultBuild'):
            Help(t + ' ')

    Help("""

All default targets will be built.  You can specify any target(s) to build:

    $ scons [options] [target]
===============================================================================
""")

env.AddMethod(PrintTargets)


env.SetDir(env.GetLaunchDir())
env['ROOT_DIR'] = env.GetLaunchDir() + '/..'

# make 'env' available to all other build scripts to Import()
Export('env')

####################################################################i#
# Generate the iotivity.pc file from iotivity.pc.in file
######################################################################
pc_file = env.get('SRC_DIR') + '/iotivity.pc.in'

user_prefix = env.get('PREFIX')
user_lib = env.get('LIB_INSTALL_DIR')

if not user_prefix:
    try:
        user_prefix = env.get('BUILD_DIR').encode('string_escape')
    except LookupError:
        user_prefix = env.get('BUILD_DIR').encode('unicode_escape')

if not user_lib:
    user_lib = '$${prefix}/lib'

defines = []
if env.get('LOGGING'):
    defines.append('-DTB_LOG=1')

if env.get('ROUTING') == 'GW':
    defines.append('-DROUTING_GATEWAY=1')
elif env.get('ROUTING') == 'EP':
    defines.append('-DROUTING_EP=1')

libs = []
if env.get('WITH_TCP'):
    defines.append('-DTCP_ADAPTER=1')
    if env.get('SECURED') == '1':
        defines.append('-D__WITH_TLS__=1')

if env.get('SECURED') == '1':
    defines.append('-D__WITH_DTLS__=1')
    if env.get('EXC_PROV_SUPPORT') == '0':
        libs.append('-locpmapi')

pc_vars = {
    '\@VERSION\@': project_version,
    '\@PREFIX\@': user_prefix,
    '\@EXEC_PREFIX\@': user_prefix,
    '\@LIB_INSTALL_DIR\@': user_lib,
    '\@DEFINES\@': " ".join(defines),
    '\@LIBS\@': " ".join(libs)
}

env.Substfile(pc_file, SUBST_DICT=pc_vars)

######################################################################
# Setting global compiler flags
######################################################################
target_transport = env.get('TARGET_TRANSPORT')
with_mq = env.get('WITH_MQ')
with_ra = env.get('WITH_RA')
with_tcp = env.get('WITH_TCP')
rd_mode = env.get('RD_MODE')
with_ra_ibb = env.get('WITH_RA_IBB')

env.AppendUnique(LIBPATH = [env.get('BUILD_DIR')])

if not env.get('PREFIX') and not env.get('LIB_INSTALL_DIR'):
   env.AppendUnique(LIBPATH = [env.get('BUILD_DIR') + '/deploy'])

if (env.get('WITH_UPSTREAM_LIBCOAP') == '1'):
    env.AppendUnique(CPPDEFINES=['WITH_UPSTREAM_LIBCOAP'])

if (target_os not in ['windows']):
    env.AppendUnique(CPPDEFINES=['WITH_POSIX'])

if (target_os in ['darwin', 'ios']):
    env.AppendUnique(CPPDEFINES=['_DARWIN_C_SOURCE'])

if (env.get('SECURED') == '1'):
    env.AppendUnique(CPPDEFINES=['SECURED'])
    env.AppendUnique(CPPDEFINES=['__WITH_DTLS__'])

if ((env.get('SECURED') == '1') and with_tcp):
    env.AppendUnique(CPPDEFINES=['__WITH_TLS__'])

if (env.get('MULTIPLE_OWNER') == '1'):
    env.AppendUnique(CPPDEFINES=['MULTIPLE_OWNER'])

if (env.get('ROUTING') == 'GW'):
    env.AppendUnique(CPPDEFINES=['ROUTING_GATEWAY'])
elif (env.get('ROUTING') == 'EP'):
    env.AppendUnique(CPPDEFINES=['ROUTING_EP'])

if (('IP' in target_transport) or ('ALL' in target_transport)):
    env.AppendUnique(CPPDEFINES=['WITH_BWT'])

if (target_os in ['linux', 'tizen', 'android', 'yocto'] and with_tcp):
    env.AppendUnique(CPPDEFINES=['WITH_TCP'])

if (target_os in ['linux', 'tizen', 'android', 'ios', 'yocto']):
    if (('BLE' in target_transport) or ('BT' in target_transport) or
        ('ALL' in target_transport)):
        env.AppendUnique(CPPDEFINES=['WITH_TCP'])

if 'ALL' in target_transport:
    if with_ra:
        env.AppendUnique(CPPDEFINES=['RA_ADAPTER'])
    if with_tcp:
        env.AppendUnique(CPPDEFINES=['TCP_ADAPTER'])
    if (target_os in ['linux', 'yocto']):
        env.AppendUnique(
            CPPDEFINES=['IP_ADAPTER', 'NO_EDR_ADAPTER', 'LE_ADAPTER'])
    elif (target_os == 'android'):
        env.AppendUnique(CPPDEFINES=[
            'IP_ADAPTER', 'EDR_ADAPTER', 'LE_ADAPTER', 'NFC_ADAPTER'
        ])
    elif (target_os in ['webos']):
        env.AppendUnique(CPPDEFINES=[
            'IP_ADAPTER', 'NO_EDR_ADAPTER', 'NO_LE_ADAPTER'])
    elif (target_os in ['darwin', 'ios', 'msys_nt', 'windows']):
        env.AppendUnique(
            CPPDEFINES=['IP_ADAPTER', 'NO_EDR_ADAPTER', 'NO_LE_ADAPTER'])
    else:
        env.AppendUnique(
            CPPDEFINES=['IP_ADAPTER', 'EDR_ADAPTER', 'LE_ADAPTER'])
else:
    if ('BT' in target_transport):
        if target_os in ('linux', 'yocto', 'webos'):
            msg = "CA Transport BT is not supported "
            Exit(msg)
        else:
            env.AppendUnique(CPPDEFINES=['EDR_ADAPTER'])
    else:
        env.AppendUnique(CPPDEFINES=['NO_EDR_ADAPTER'])

    if ('BLE' in target_transport):
        env.AppendUnique(CPPDEFINES=['LE_ADAPTER'])
    else:
        env.AppendUnique(CPPDEFINES=['NO_LE_ADAPTER'])

    if ('IP' in target_transport):
        env.AppendUnique(CPPDEFINES=['IP_ADAPTER'])
    else:
        env.AppendUnique(CPPDEFINES=['NO_IP_ADAPTER'])

    if with_tcp:
        if (target_os in [
                'linux', 'tizen', 'android', 'ios', 'windows', 'yocto', 'webos'
        ]):
            env.AppendUnique(CPPDEFINES=['TCP_ADAPTER', 'WITH_TCP'])
        else:
            msg = "CA Transport TCP is not supported "
            Exit(msg)
    else:
        env.AppendUnique(CPPDEFINES=['NO_TCP_ADAPTER'])

    if ('NFC' in target_transport):
        if (target_os == 'android'):
            env.AppendUnique(CPPDEFINES=['NFC_ADAPTER'])
        else:
            msg = "CA Transport NFC is not supported "
            Exit(msg)
    else:
        env.AppendUnique(CPPDEFINES=['NO_NFC_ADAPTER'])

if ('SUB' in with_mq):
    env.AppendUnique(CPPDEFINES=['MQ_SUBSCRIBER', 'WITH_MQ'])

if ('PUB' in with_mq):
    env.AppendUnique(CPPDEFINES=['MQ_PUBLISHER', 'WITH_MQ'])

if ('BROKER' in with_mq):
    env.AppendUnique(CPPDEFINES=['MQ_BROKER', 'WITH_MQ'])

env.AppendUnique(CPPDEFINES={'OC_LOG_LEVEL': env.get('LOG_LEVEL')})

if env.get('LOGGING'):
    env.AppendUnique(CPPDEFINES=['TB_LOG'])

if env.get('WITH_CLOUD') and with_tcp:
    env.AppendUnique(CPPDEFINES=['WITH_CLOUD'])

if 'CLIENT' in rd_mode:
    env.AppendUnique(CPPDEFINES=['RD_CLIENT'])

if 'SERVER' in rd_mode:
    env.AppendUnique(CPPDEFINES=['RD_SERVER'])

if with_ra_ibb:
    env.AppendUnique(CPPDEFINES=['RA_ADAPTER_IBB'])

if env.get('RELEASE'):
    env.AppendUnique(CPPDEFINES=['NDEBUG'])

env.SConscript('external_builders.scons')
######################################################################
# Link scons to Yocto cross-toolchain ONLY when target_os is yocto
######################################################################
if target_os in ['yocto', 'webos']:
    '''
    This code injects Yocto cross-compilation tools+flags into the scons
    construction environment in order to invoke the relevant tools while
    performing a build.
    '''
    import os.path
    try:
        CC = os.environ['CC']
        target_prefix = CC.split()[0]
        target_prefix = target_prefix[:-3]
        tools = {
            "CC": target_prefix + "gcc",
            "CXX": target_prefix + "g++",
            "AS": target_prefix + "as",
            "LD": target_prefix + "ld",
            "GDB": target_prefix + "gdb",
            "STRIP": target_prefix + "strip",
            "RANLIB": target_prefix + "ranlib",
            "OBJCOPY": target_prefix + "objcopy",
            "OBJDUMP": target_prefix + "objdump",
            "AR": target_prefix + "ar",
            "NM": target_prefix + "nm",
            "M4": "m4",
            "STRINGS": target_prefix + "strings"
        }
        PATH = os.environ['PATH'].split(os.pathsep)
        for tool in tools:
            if tool in os.environ:
                for path in PATH:
                    if os.path.isfile(os.path.join(path, tools[tool])):
                        env[tool] = os.path.join(path, os.environ[tool])
                        break
        env['CROSS_COMPILE'] = target_prefix[:-1]
    except:
        msg = "ERROR in Yocto cross-toolchain environment"
        Exit(msg)
    '''
    Now reset TARGET_OS to linux so that all linux specific build configurations
    hereupon apply for the entirety of the build process.
    '''
    env['TARGET_OS'] = 'linux'
    '''
    We want to preserve debug symbols to allow BitBake to generate both DEBUG and
    RELEASE packages for OIC.
    '''
    env.AppendUnique(CCFLAGS=['-g'])
    '''
    Additional flags to pass to the Yocto toolchain.
    '''
    env.AppendUnique(CPPDEFINES=['__linux__', '_GNU_SOURCE'])
    env.AppendUnique(CFLAGS=['-std=gnu99'])
    env.AppendUnique(CCFLAGS=['-Wall', '-Wextra', '-fPIC'])
    env.AppendUnique(LIBS=['dl', 'pthread', 'uuid'])
else:
    '''
    If target_os is not Yocto, continue with the regular build process
    '''
    # Load config of target os
    env.SConscript(target_os + '/SConscript')

if env.get('CROSS_COMPILE'):
    env.Append(RPATH=env.Literal('\\$$ORIGIN'))
else:
    env.Append(RPATH=env.get('BUILD_DIR'))

# Delete the temp files of configuration
if env.GetOption('clean'):
    dir = env.get('SRC_DIR')

    if os.path.exists(dir + '/config.log'):
        Execute(Delete(dir + '/config.log'))
    if os.path.exists(dir + '/.sconsign.dblite'):
        Execute(Delete(dir + '/.sconsign.dblite'))
    if os.path.exists(dir + '/.sconf_temp'):
        Execute(Delete(dir + '/.sconf_temp'))

######################################################################
# Check for PThreads support
######################################################################
import iotivityconfig
from iotivityconfig import *

conf = Configure(
    env, custom_tests={'CheckPThreadsSupport': iotivityconfig.check_pthreads})

# Identify whether we have pthreads support, which is necessary for
# threading and mutexes.  This will set the environment variable
# POSIX_SUPPORTED, 1 if it is supported, 0 otherwise
conf.CheckPThreadsSupport()
env = conf.Finish()

######################################################################
# Generate Cbor from json files
######################################################################
json2cbor = env.get('BUILD_DIR') + 'resource/csdk/security/tool/json2cbor' + env.get('PROGSUFFIX')

def generate_actions(source, target, env, for_signature):
    Depends(target, json2cbor)
    return " %s %s %s" % (json2cbor, source[0], target[0])

builder = Builder(generator = generate_actions,
                  suffix = '.dat',
                  src_suffix = '.json')

env.Append(BUILDERS = {'Cbor' : builder})


def ScanJSON(env, directory=None):
    targets = []
    if not directory:
        directory = Dir('.').srcnode().path
    if env.GetOption('clean') or env.get('SECURED') != '1':
        return targets
    dst_dir = env.get('BUILD_DIR') + '/' + directory + '/'
    src_dir = env.get('SRC_DIR') + '/' + directory + '/'
    for json_file in Glob('*.json'):
        filename = str(json_file.name)
        src = src_dir + filename
        dst = dst_dir + filename
        targets += Command(dst, src, Copy("$TARGET", "$SOURCE"))
        if env.get('CROSS_COMPILE') == None:
            # Copy back compiled files to sources (to be used when compilation is not possible)
            cbor_file = env.Cbor(json_file)
            targets.append(cbor_file)
            cbor_file = Flatten(cbor_file)[0].name
            src = dst_dir + cbor_file
            dst = src_dir + cbor_file
            Command(dst, src, Copy("$TARGET", "$SOURCE"))
        else:
            # Can compile files at build time, so rely on previous generated files
            cbor_file = re.sub('\.json$', '.dat', filename)
            src = src_dir + cbor_file
            dst = dst_dir + cbor_file
            targets += Command(dst, src, Copy("$TARGET", "$SOURCE"))
    return targets

env.AddMethod(ScanJSON)

######################################################################
env.SConscript('external_libs.scons')

# these variables depend on a subsidiary script having set a path to
# use as the default. Such paths may include embedded version strings,
# for example, and we want to not embed those all over, so defer setting
# the help until we're back from those scripts. Then we can finally
# build the help message. However, it's also possible that we never
# needed to call those subsidary scripts, and then we come back with
# values unset, so deal with that as well.
if env.get('ANDROID_NDK'):
    android_ndk = env['ANDROID_NDK']
else:
    android_ndk = None
if env.get('ANDROID_GRADLE'):
    android_gradle = env['ANDROID_GRADLE']
else:
    android_gradle = None
if env.get('ANDROID_HOME'):
    android_sdk = env['ANDROID_HOME']
else:
    android_sdk = None
help_vars.AddVariables(
    PathVariable('ANDROID_NDK',
                 'Android NDK path',
                 default=android_ndk,
                 validator=PathVariable.PathAccept),
    PathVariable('ANDROID_GRADLE',
                 'Gradle executable location',
                 default=android_gradle,
                 validator=PathVariable.PathIsFile),
    PathVariable('ANDROID_HOME',
                 'Android SDK path',
                 default=android_sdk,
                 validator=PathVariable.PathAccept),
)
help_vars.Update(env)
Help(help_vars.GenerateHelpText(env, sort=True))
# Replicate change that occured after help_var initialisation from env
if target_os == "yocto":
    env['TARGET_OS'] = 'linux'

Return('env')
